/**
 * 判断IP是否为内网IP
 * from http://nodejs.cn/api/dns.html#dns_dns_lookup_hostname_options_callback
 *      https://zh.wikipedia.org/wiki/%E4%B8%93%E7%94%A8%E7%BD%91%E7%BB%9C
 *
 * ipv4 私有网段 10.0.0.0 - 10.255.255.255
 *             172.16.0.0 - 172.31.255.255
 * 						 192.168.0.0 - 192.168.255.255
 * 						 127.0.0.0 - 127.255.255.255
 *
 * ipv6 私有网段 ::
 * 						 ::1
 * 						 fc00:: - fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 * 						 fe80:: - febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 * 						 ff00:: - ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 *
 * ipv6转ipv4的网段
 * 						 ::ffff:0:0 - ::ffff:ffff:ffff【后两段】
 * 						 64:ff9b:: - 64:ff9b::ffff:ffff
 * 						 2002:: - 2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff 【第二三段】
 *
 */

import debugCheck from '@kuiper-helpers/debug-check/dist';
import JSBI from 'jsbi';
import isIP from './isIP';

const ipv4Rule = '0.0.0.0';
const ipv4Rule2 = '255.255.255.255';
const ipv4RangeBegin = ['10.0.0.0', '172.16.0.0', '192.168.0.0', '127.0.0.0'];
const ipv4RangeEnd = [
	'10.255.255.255',
	'172.31.255.255',
	'192.168.255.255',
	'127.255.255.255',
];

const ipv6Rule = '::';
const ipv6Rule2 = '::1';
const ipv6RangeBegin = ['fc00::', 'fe80::', 'ff00::'];
const ipv6RangeEnd = [
	'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
	'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
	'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
];

const zoneIndex = '%[0-9a-z]{1,}';
const ipv4Part = '(0?\\d+|0x[a-f0-9]+)';
const ipv6Part = '(?:[0-9a-f]+::?)+';
const ipv6Regexes = {
	zoneIndex: new RegExp(zoneIndex, 'i'),
	native: new RegExp(
		`^(::)?(${ipv6Part})?([0-9a-f]+)?(::)?(${zoneIndex})?$`,
		'i'
	),
	deprecatedTransitional: new RegExp(
		`^(?:::)(${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?)$`,
		'i'
	),
	transitional: new RegExp(
		`^((?:${ipv6Part})|(?:::)(?:${ipv6Part})?)${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?$`,
		'i'
	),
};

function splitIpv4(ip) {
	const b = ip.split('.');
	if (b.length === 4) {
		b[0] = b[0].substring(b[0].lastIndexOf(':') + 1);
		b[3] = b[3].indexOf(':') > -1 ? b[3].substring(0, b[3].indexOf(':')) : b[3];
		return b.join('.');
	}
	return false;
}

function ipV4Int(ip) {
	const ipStr = ip
		.split('.')
		.map(item => {
			return Number(item).toString(16).padStart(2, '0');
		})
		.join('');
	return parseInt(ipStr, 16);
}

function isPrivateIpV4(ip) {
	const target = ipV4Int(ip);
	let result = ipv4RangeBegin.some((item, index) => {
		const start = ipV4Int(item);
		const end = ipV4Int(ipv4RangeEnd[index]);
		return target >= start && target <= end;
	});
	if (target === ipV4Int(ipv4Rule) || target === ipV4Int(ipv4Rule2))
		result = true;
	return result;
}

function isPrivateIpV6(ip) {
	const hasIpv4 = ipv6to4(ip);
	if (hasIpv4) {
		return isPrivateIpV4(hasIpv4);
	} else {
		const target = expandIPv6(ip).num;
		let result = ipv6RangeBegin.some((item, index) => {
			const start = expandIPv6(item).num;
			const end = expandIPv6(ipv6RangeEnd[index]).num;
			return (
				JSBI.greaterThanOrEqual(target, start) &&
				JSBI.lessThanOrEqual(target, end)
			);
		});
		if (
			target === expandIPv6(ipv6Rule).num ||
			target === expandIPv6(ipv6Rule2).num
		)
			result = true;
		return result;
	}
}

const rule1Start = expandIPv6('::ffff:0:0').num;
const rule1End = expandIPv6('::ffff:ffff:ffff').num;
const rule2Start = expandIPv6('64:ff9b::').num;
const rule2End = expandIPv6('64:ff9b::ffff:ffff').num;
const rule3Start = expandIPv6('2002::').num;
const rule3End = expandIPv6('2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff').num;

function ipv6to4(ip) {
	const hasIpv4 = splitIpv4(ip);
	if (hasIpv4) {
		return hasIpv4;
	} else {
		const ipNum = expandIPv6(ip).num;
		if (
			(JSBI.lessThanOrEqual(rule1Start, ipNum) &&
				JSBI.lessThanOrEqual(ipNum, rule1End)) ||
			(JSBI.lessThanOrEqual(rule2Start, ipNum) &&
				JSBI.lessThanOrEqual(ipNum, rule2End))
		) {
			return ipv4Format(expandIPv6(ip).arr.slice(6));
		} else if (
			JSBI.lessThanOrEqual(rule3Start, ipNum) &&
			JSBI.lessThanOrEqual(ipNum, rule3End)
		) {
			return ipv4Format(expandIPv6(ip).arr.slice(1, 3));
		}
		return false;
	}
	function ipv4Format(arr) {
		return arr
			.reduce((result, item) => {
				result.push(parseInt(item.substring(0, 2), 16));
				result.push(parseInt(item.substring(2), 16));
				return result;
			}, [])
			.join('.');
	}
}

function expandIPv6(string, parts = 8) {
	// More than one '::' means invalid adddress
	if (string.indexOf('::') !== string.lastIndexOf('::')) {
		return null;
	}

	let colonCount = 0;
	let lastColon = -1;
	let zoneId = (string.match(ipv6Regexes.zoneIndex) || [])[0];
	let replacement, replacementCount;

	// Remove zone index and save it for later
	if (zoneId) {
		zoneId = zoneId.substring(1);
		string = string.replace(/%.+$/, '');
	}

	// How many parts do we already have?
	while ((lastColon = string.indexOf(':', lastColon + 1)) >= 0) {
		colonCount++;
	}

	// 0::0 is two parts more than ::
	if (string.substr(0, 2) === '::') {
		colonCount--;
	}

	if (string.substr(-2, 2) === '::') {
		colonCount--;
	}

	// The following loop would hang if colonCount > parts
	if (colonCount > parts) {
		return null;
	}

	// replacement = ':' + '0:' * (parts - colonCount)
	replacementCount = parts - colonCount;
	replacement = ':';
	while (replacementCount--) {
		replacement += '0:';
	}

	// Insert the missing zeroes
	string = string.replace('::', replacement);

	// Trim any garbage which may be hanging around if :: was at the edge in
	// the source strin
	if (string[0] === ':') {
		string = string.slice(1);
	}

	if (string[string.length - 1] === ':') {
		string = string.slice(0, -1);
	}

	parts = (function () {
		const ref = string.split(':');
		const results = [];

		for (let i = 0; i < ref.length; i++) {
			const item = ref[i].padStart(4, '0');
			results.push(item);
		}

		return results;
	})();

	return {
		arr: parts,
		num: JSBI.BigInt('0X' + parts.join('')), // eslint-disable-line
	};
}

export default function ipLimit(ip) {
	if (debugCheck('allowPrivateIP')) return false;

	if (ip) {
		if (isIP(ip) === 4) {
			return isPrivateIpV4(ip);
		} else if (isIP(ip) === 6) {
			return isPrivateIpV6(ip);
		}
		return false;
	}

	return ip;
}
